/*
 *    Copyright (c) 2018-2025, lengleng All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * Neither the name of the pig4cloud.com developer nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * Author: lengleng (wangiegie@gmail.com)
 */

package com.pig4cloud.pigx.wallet.controller;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.customize.i18n.util.MessageUtils;
import com.pig4cloud.pigx.common.customize.redis.utils.RedisUtils;
import com.pig4cloud.pigx.common.customize.security.utils.SecurityUtil;
import com.pig4cloud.pigx.common.customize.utils.CommonUtil;
import com.pig4cloud.pigx.common.excel.annotation.ResponseExcel;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import com.pig4cloud.pigx.contract.constant.I18nMessages;
import com.pig4cloud.pigx.user.entity.User;
import com.pig4cloud.pigx.wallet.api.feign.RemoteRippleUserService;
import com.pig4cloud.pigx.wallet.bo.WalletChangeContactAccountBO;
import com.pig4cloud.pigx.wallet.dto.WalletContractAccountChangeDTO;
import com.pig4cloud.pigx.wallet.entity.ContractAccount;
import com.pig4cloud.pigx.wallet.entity.ContractAccountDetail;
import com.pig4cloud.pigx.wallet.enums.WalletContractAccountDetailEnum;
import com.pig4cloud.pigx.wallet.service.ContractAccountDetailService;
import com.pig4cloud.pigx.wallet.service.ContractAccountService;
import io.seata.spring.annotation.GlobalLock;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 合约账户
 *
 * @since 2022-08-16 16:01:11
 */
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/contractaccount")
@Api(value = "contractaccount", tags = "合约账户管理")
public class ContractAccountController {

    private final ContractAccountService contractAccountService;
    private final ContractAccountDetailService contractAccountDetailService;
    private final RemoteRippleUserService remoteRippleUserService;

    private final CommonUtil commonUtil;
    private final SecurityUtil securityUtil;
    private final RedisUtils redisUtils;

    /**
     * 分页查询
     *
     * @param page            分页对象
     * @param contractAccount 合约账户
     */
    @ApiOperation(value = "分页查询", notes = "分页查询")
    @GetMapping("/page")
    @PreAuthorize("@pms.hasPermission('wallet_contractaccount_view')")
    public R<Page<ContractAccount>> getContractAccountPage(Page<ContractAccount> page, ContractAccount contractAccount) {
        List<Long> userIdList = null;

        if (StrUtil.isNotBlank(contractAccount.getEmail())) {
            List<User> userList = remoteRippleUserService.getUserByEmail(contractAccount.getEmail()).getData();
            if (CollUtil.isNotEmpty(userList)) {
                userIdList = userList.stream().map(User::getUid).collect(Collectors.toList());
            }
        }

        Page<ContractAccount> list = contractAccountService.page(page, new LambdaQueryWrapper<ContractAccount>()
                .eq(contractAccount.getUserId() != null, ContractAccount::getUserId, contractAccount.getUserId())
                .in(CollUtil.isNotEmpty(userIdList), ContractAccount::getUserId, userIdList)
        );

        List<ContractAccount> records = list.getRecords();
        List<Long> collect = records.stream().map(ContractAccount::getUserId).collect(Collectors.toList());
        List<User> data = remoteRippleUserService.getUserList(collect, SecurityConstants.FROM_IN).getData();

        records.forEach(record -> {
            data.forEach(item -> {
                if (record.getUserId().equals(item.getUid())) {
                    record.setEmail(item.getEmail());
                }
            });

            record.setWalletBalance(record.getWalletBalance().setScale(2, RoundingMode.DOWN));
            record.setFrozen(record.getFrozen().setScale(2, RoundingMode.DOWN));
            record.setMarginBalance(record.getMarginBalance().setScale(2, RoundingMode.DOWN));
            record.setTotalAssets(record.getMarginBalance());

        });

        return R.ok(list);
    }


    /**
     * 通过id查询合约账户
     *
     * @param id id
     * @return R
     */
    @ApiOperation(value = "通过id查询", notes = "通过id查询")
    @GetMapping("/{id}")
    @PreAuthorize("@pms.hasPermission('wallet_contractaccount_view')")
    public R<ContractAccount> getById(@PathVariable("id") Long id) {
        return R.ok(contractAccountService.getById(id));
    }

    /**
     * 新增合约账户
     *
     * @param contractAccount 合约账户
     * @return R
     */
    @ApiOperation(value = "新增合约账户", notes = "新增合约账户")
    @SysLog("新增合约账户")
    @PostMapping
    @PreAuthorize("@pms.hasPermission('wallet_contractaccount_add')")
    public R<Boolean> save(@RequestBody ContractAccount contractAccount) {
        return R.ok(contractAccountService.save(contractAccount));
    }

    /**
     * 修改合约账户
     *
     * @param contractAccount 合约账户
     * @return R
     */
    @ApiOperation(value = "修改合约账户", notes = "修改合约账户")
    @SysLog("修改合约账户")
    @PutMapping
    @PreAuthorize("@pms.hasPermission('wallet_contractaccount_edit')")
    public R<Boolean> updateById(@RequestBody ContractAccount contractAccount) {

        RLock lock = redisUtils.getLock(String.valueOf(contractAccount.getUserId()), null);

        try {
            ContractAccount contractAccountUpdate = new ContractAccount();
            contractAccountUpdate.setId(contractAccount.getId());
            contractAccountUpdate.setWalletBalance(contractAccount.getWalletBalance());
            contractAccountUpdate.setFrozen(contractAccount.getFrozen());
            contractAccountUpdate.setMarginBalance(contractAccount.getWalletBalance().add(contractAccount.getFrozen()));

            contractAccountService.updateById(contractAccountUpdate);
            return R.ok();
        } finally {
            redisUtils.releaseLock(lock);
        }
    }

    /**
     * 通过id删除合约账户
     *
     * @param id id
     * @return R
     */
    @ApiOperation(value = "通过id删除合约账户", notes = "通过id删除合约账户")
    @SysLog("通过id删除合约账户")
    @DeleteMapping("/{id}")
    @PreAuthorize("@pms.hasPermission('wallet_contractaccount_del')")
    public R<Boolean> removeById(@PathVariable Long id) {
        return R.ok(contractAccountService.removeById(id));
    }


    /**
     * 导出excel 表格
     *
     * @param contractAccount 查询条件
     * @return excel 文件流
     */
    @ResponseExcel
    @GetMapping("/export")
    @PreAuthorize("@pms.hasPermission('wallet_contractaccount_export')")
    public List<ContractAccount> export(ContractAccount contractAccount) {
        return contractAccountService.list(Wrappers.query(contractAccount));
    }

    /**
     * 查询合约账户最新信息，包括总金额、冻结金额、可用金额等
     */
    @Inner
    @ApiOperation(value = "查询合约账户最新信息，包括总金额、冻结金额、可用金额等", notes = "查询合约账户最新信息，包括总金额、冻结金额、可用金额等")
    @PostMapping("/contract/info")
    public R<ContractAccount> contractInfo(@RequestBody WalletContractAccountChangeDTO walletContractAccountChangeDTO) {
        ContractAccount contractAccount = contractAccountService.getOne(new LambdaQueryWrapper<ContractAccount>()
                .eq(ContractAccount::getUserId, walletContractAccountChangeDTO.getUserId())
        );

        return R.ok(contractAccount);
    }

    /**
     * 查询合约账户并锁定
     *
     * @param dto dto
     * @return {@link R< ContractAccount>}
     * @since 2023/2/24 14:15
     */
    @Inner
    @ApiOperation(value = "查询合约账户并锁定", notes = "查询合约账户并锁定")
    @PostMapping("/contract/info/lock")
    @GlobalLock
    @Transactional(rollbackFor = Throwable.class)
    public R<ContractAccount> contractInfoLock(@RequestBody WalletContractAccountChangeDTO dto) {
        ContractAccount contractAccount = contractAccountService.getOne(new LambdaQueryWrapper<ContractAccount>()
                .eq(ContractAccount::getUserId, dto.getUserId())
                .last("for update")
        );

        // 不为空时，通过执行update语句来添加分布式锁
        if (!Objects.isNull(contractAccount)) {
            if (!contractAccount.updateById()) {
                throw new RuntimeException("查询合约账户添加分布式锁时失败");
            }
        }

        return R.ok(contractAccount);
    }

    /**
     * 合约账户修改额度
     *
     * @param dtoList 参数集合
     * @return {@link R}
     * @since 2022/9/13 17:36
     */
    @Inner
    @ApiOperation(value = "合约账户修改额度", notes = "合约账户修改额度")
    @PostMapping("/change")
    @GlobalLock
    @Transactional(rollbackFor = Throwable.class)
    public R<BigDecimal> change(@RequestBody List<WalletContractAccountChangeDTO> dtoList) {
        Map<Long, List<WalletContractAccountChangeDTO>> map = dtoList.stream()
                .collect(Collectors.groupingBy(WalletContractAccountChangeDTO::getUserId));

        // 集中处理变更记录
        List<ContractAccountDetail> detailList = new ArrayList<>(dtoList.size());
        // 遍历处理
        map.forEach((userId, list) -> {
            // redis锁，锁定钱包
            RLock lock = redisUtils.waitLock(String.valueOf(userId), null);

            try {
                // 查询用户钱包，并锁定
                LambdaQueryWrapper<ContractAccount> query = new LambdaQueryWrapper<ContractAccount>()
                        .eq(ContractAccount::getUserId, userId)
                        .last(true, "for update");
                ContractAccount contractAccount = contractAccountService.getOne(query);

                // 转换为bo
                WalletChangeContactAccountBO bo = new WalletChangeContactAccountBO();
                bo.setId(contractAccount.getId());
                bo.setTotal(contractAccount.getMarginBalance());
                bo.setAvailable(contractAccount.getWalletBalance());
                bo.setFrozen(contractAccount.getFrozen());
                bo.setPositionMargin(contractAccount.getPositionMargin());
                list.forEach(dto -> {
					/*
					 设置8位精度，与mysql一样四舍五入
					 注释原因：不能在此处设置精度，直接使用传入的金额就行，如果设置了精度，会存在以下问题。
					 例：
					 	前提：调用方没有设置精度，但在此处设置了精度
						情景：订单中冻结额度为10，钱包中冻结额度为10，数据库表对应字段精度都是2，现在需要解冻0.555。
						订单方没有设置精度，10 - 0.555 = 9.445，数据库会四舍五入为9.45。
						钱包设置了精度，将0.55四舍五入为0.6，10 - 0.6 = 9.4。
						造成两处数据不统一
					 */
                    // dto.setAmount(CommonUtil.setUSDTScale(dto.getAmount()));

                    // 冻结
                    // 相关业务逻辑：下单
                    if (Objects.equals(WalletContractAccountDetailEnum.OperationType.FREEZE, dto.getOperationType())) {
                        bo.setAvailable(bo.getAvailable().subtract(dto.getAmount()));
                        bo.setFrozen(bo.getFrozen().add(dto.getAmount()));

                    } else if (Objects.equals(WalletContractAccountDetailEnum.OperationType.UN_FREEZE, dto.getOperationType())) {
                        // 解冻
                        // 相关业务逻辑：订单完全成交、撤销、过期
                        bo.setAvailable(bo.getAvailable().add(dto.getAmount()));
                        bo.setFrozen(bo.getFrozen().subtract(dto.getAmount()));

                    } else if (Objects.equals(WalletContractAccountDetailEnum.OperationType.ADD, dto.getOperationType())) {
                        // 增加
                        // 增加持仓保证金
                        if (Objects.equals(WalletContractAccountDetailEnum.BusinessType.POSITION_MARGIN, dto.getBusinessType())) {

                            bo.setPositionMargin(bo.getPositionMargin().add(dto.getAmount()));
                            // 拥有冻结额度，扣除冻结额度
                            // 相关业务逻辑：开仓
                            if (Boolean.TRUE.equals(dto.getHasFrozen())) {
                                bo.setFrozen(bo.getFrozen().subtract(dto.getAmount()));

                            } else {
                                // 没有冻结额度，扣除可用额度
                                // 相关业务逻辑：调整保证金（增加）、全仓自动调整（增加）
                                bo.setAvailable(bo.getAvailable().subtract(dto.getAmount()));
                            }

                        } else {
                            // 其它
                            bo.setTotal(bo.getTotal().add(dto.getAmount()));
                            bo.setAvailable(bo.getAvailable().add(dto.getAmount()));

                            // 生成合约账户明细
                            ContractAccountDetail detail = getContractAccountDetail(dto);
                            detailList.add(detail);
                        }

                    } else if (Objects.equals(WalletContractAccountDetailEnum.OperationType.MINUS, dto.getOperationType())) {
                        // 减少
                        // 减少持仓保证金
                        // 相关业务逻辑：平仓、调整保证金（减少）、全仓自动调整（减少）
                        if (Objects.equals(WalletContractAccountDetailEnum.BusinessType.POSITION_MARGIN, dto.getBusinessType())) {
                            bo.setAvailable(bo.getAvailable().add(dto.getAmount()));
                            bo.setPositionMargin(bo.getPositionMargin().subtract(dto.getAmount()));

                        } else {
                            // 资金费率处理 如果可用余额不够,则有多少扣多少,并返回实际扣除额度
                            if (Objects.equals(WalletContractAccountDetailEnum.BusinessType.FUNDING_COSTS, dto.getBusinessType())) {
                                if (dto.getAmount().compareTo(bo.getAvailable()) > 0) {
                                    dto.setAmount(bo.getAvailable());
                                }
                            }
                            // 其它
                            bo.setTotal(bo.getTotal().subtract(dto.getAmount()));
                            // 扣除冻结额度
                            // 相关逻辑：开仓
                            if (Boolean.TRUE.equals(dto.getHasFrozen())) {
                                bo.setFrozen(bo.getFrozen().subtract(dto.getAmount()));
                            } else {
                                // 扣除可用额度
                                // 相关逻辑：平仓、强平、盈亏、资金费用、爆仓清算费
                                bo.setAvailable(bo.getAvailable().subtract(dto.getAmount()));
                            }

                            // 生成合约账户明细
                            ContractAccountDetail detail = getContractAccountDetail(dto);
                            detailList.add(detail);
                        }

                    } else {
                        throw new RuntimeException(MessageUtils.get(I18nMessages.UNRECOGNIZED_OPERATIONI_TYPE));
                    }
                });

                // 校验额度。批量操作时，需要算出最终结果后，再判断额度是否足够
                // 判断是否是平仓/强平时发来的参数，通过是否包含未实现盈亏判断
                boolean isClose = list.stream().anyMatch(i -> Objects.equals(WalletContractAccountDetailEnum.BusinessType.PROFIT, i.getBusinessType()));
                // 不是平仓/强平时，校验总额度、可用额度
                if (!isClose) {
                    if (bo.getTotal().compareTo(BigDecimal.ZERO) < 0) {
                        // 合约账户额 度不足
                        throw new RuntimeException(MessageUtils.get(I18nMessages.INSUFFICIENT_CONTRACT_ACCOUNT));
                    }
                    if (bo.getAvailable().compareTo(BigDecimal.ZERO) < 0) {
                        // 合约账户可用额度不足
                        throw new RuntimeException(MessageUtils.get(I18nMessages.INSUFFICIENT_CONTRACT_ACCOUNT_AVILABILITY));
                    }
                }

                if (bo.getPositionMargin().compareTo(BigDecimal.ZERO) < 0) {
                    // 合约账户持仓保证金额度不足
                    throw new RuntimeException(MessageUtils.get(I18nMessages.CONTRACT_ACCOUNT_HOLDING_MARGIN_IS_INSUFFICIENT));
                }
                if (bo.getFrozen().compareTo(BigDecimal.ZERO) < 0) {
                    // 合约账户冻结额度不足
                    throw new RuntimeException(MessageUtils.get(I18nMessages.FROZEN_AMOUNT_OF_THE_CONTRACT_ACCOUNT_IS_INSUFFICIENT));
                }

                // 修改合约账户
                ContractAccount updateParam = new ContractAccount();
                updateParam.setId(bo.getId());
                updateParam.setUserId(userId);
                updateParam.setMarginBalance(bo.getTotal());
                updateParam.setWalletBalance(bo.getAvailable());
                updateParam.setFrozen(bo.getFrozen());
                updateParam.setPositionMargin(bo.getPositionMargin());

				/* 调试日志输出
				String log = "userId-%s，合约账户额度变更%s：total-%s、available-%s、frozen-%s、positionMargin-%s";

				System.err.printf((log) + "%n",
						contractAccount.getUserId(),
						"前",
						contractAccount.getMarginBalance(),
						contractAccount.getWalletBalance(),
						contractAccount.getFrozen(),
						contractAccount.getPositionMargin());

				System.err.printf((log) + "%n",
						contractAccount.getUserId(),
						"后",
						updateParam.getMarginBalance(),
						updateParam.getWalletBalance(),
						updateParam.getFrozen(),
						updateParam.getPositionMargin());*/

                if (!contractAccountService.updateById(updateParam)) {
                    // 修改账户额度失败
                    throw new RuntimeException(MessageUtils.get(I18nMessages.FAILED_TO_MODIFY_THE_ACCOUNT_LIMIT));
                }
            } finally {
                redisUtils.releaseLock(lock);
            }
        });

        // 批量新增变更记录
        if (CollectionUtils.isNotEmpty(detailList) && !contractAccountDetailService.saveBatch(detailList)) {
            // 修改账户额度失败
            throw new RuntimeException(MessageUtils.get(I18nMessages.FAILED_TO_MODIFY_THE_ACCOUNT_LIMIT));
        }
        // 如果是资金费率,返回本次实际扣除金额
        if (detailList.size() == 1 &&
                detailList.get(0).getBusinessType().equals(WalletContractAccountDetailEnum.BusinessType.FUNDING_COSTS.getValue())) {
            return R.ok(detailList.get(0).getAmount());
        }
        return R.ok();
    }

    /**
     * 根据合约账户变更参数生成变更记录
     *
     * @param dto 合约账户变更参数
     * @return {@link ContractAccountDetail}
     * @since 2022/9/13 16:00
     */
    private ContractAccountDetail getContractAccountDetail(WalletContractAccountChangeDTO dto) {
        ContractAccountDetail detail = new ContractAccountDetail();
        detail.setUserId(dto.getUserId());
        detail.setBusinessType(dto.getBusinessType().getValue());
        detail.setOperationType(dto.getOperationType().getValue());
        detail.setAmount(dto.getAmount());
        detail.setFeeRate(dto.getFeeRate());
        detail.setSymbolId(dto.getSymbolId());
        detail.setSymbolName(dto.getSymbolName());
        securityUtil.initCreateAbout(detail);
        return detail;
    }

}
